#!/bin/bash
# Casc: Certification Authority 

# Copyright (C) 2009 Bertera Pietro <pietro@bertera.it>

# This file may be distributed and/or modified under the terms of
# the GNU General Public License version 2 as published by
# the Free Software Foundation.
# This file is distributed without any warranty; without even the implied
# warranty of merchantability or fitness for a particular purpose.

# Inizializzare una CA:
# ./casc newca
#
# Creare un certtificato con password:
# ./casc newcert
#
# Creare un certificato senza password:
# ./cas newcert nopass
#
# Revocare un certificato:
# ./cas revoke
#
# Generare la CRL:
# ./cas gencrl
#
# Pubblicare la CRL usando tutti i plugin:
# ./cas pubcrl all
#
# Pubblicare la CLR usasndo il plugin postfix
# ./cas pubcrl postfix
#
# Verificare la scadenza dei certificati:
# ./cas check

. ./casc.conf

REQ="openssl req -config $OPENSSL_CONF"
CA="openssl ca -config $OPENSSL_CONF"
CRL="openssl crl"
PKCS12="openssl pkcs12"
X509="openssl x509"
TAR=/bin/tar

defined () { [ "${!1-X}" == "${!1-Y}" ]; }

error() {
	print "Error:" $*
	exit 1
}

syncronize () {
	echo "Sync"
}	

decrypt () {
	echo "Decrypting $1"
	
	gpg_opt=""
	if [ -f $GPG_PASSFILE ]; then
		gpg_opt="--passphrase-file $GPG_PASSFILE"
	fi	
	
	$GPG $gpg_opt -o $2 -d $1
}

encrypt () {
	RECIPIENTS=""
	for r in $GPG_RECIPIENTS
	do
		echo "Adding $r to encrypted recipients.."
		RECIPIENTS=" $RECIPIENTS -r $r"
	done

	echo "Encrypting data .."
	$GPG $RECIPIENTS -e $1
	rm -f $1
}

backup() {
	if [ $CA_BACKUP_TAR -a $PRIV_BACKUP_TAR -a $TAR -a $GPG -a $GPG_RECIPIENTS ]; then 
		error "No CA_BACKUP_TAR PRIV_BACKUP_TAR, TAR, GPG or GPG_RECIPIENTS defined"
	fi
	
	echo Archiving Data
	$TAR czvf $PRIV_BACKUP_TAR $PRIVATE_DIR
		
	encrypt $PRIV_BACKUP_TAR
	
	$TAR czvf $CA_BACKUP_TAR $CATOP
	
	encrypt $CA_BACKUP_TAR
	
	mv $PRIV_BACKUP_TAR.gpg $BACKUP_DIR
	mv $CA_BACKUP_TAR.gpg $BACKUP_DIR	
	exit 0
}

check_ca_pass() {
	$CA -gencrl -out /dev/null -key "$1" >/dev/null 2>&1
	return $?
}

get_ca_pass() {
	if [ -z "$CAPASS" ]; then
		while true; do
        		echo -n "Certification Authority password: "
        		read -s CAPASS
        		echo ""	
	
			`check_ca_pass "$CAPASS"` && break
			echo "Wrong CA Password."
		done
	else
		`check_ca_pass "$CAPASS"` && return
		echo "Wrong CA Password."
		exit 1
	fi
}

get_certpass()  {
	if [ -z "$PRIVPASS" ]; then
		while true; do
		   	# Fetch certificate password
			echo -n "Certificate password: "
		    	read -s PRIVPASS
			echo ""
			echo -n "Certificate password (confirm): "
			read -s PRIVPASS2
			echo ""
			if [ "$PRIVPASS" != "$PRIVPASS2" ]; then
				echo "Password mismatch!"
			else
				break
		fi
		done

	fi
}

get_sdate() {
	defined SDATE && return

	if [ -z "$SDATE" ]; then
		echo -n "Valid from (YYYY/MM/DD HH:MM:SS) if none now: "
		read SDATE
	fi
}

get_edate() {
	defined EDATE && return

	if [ -z $EDATE ]; then
		echo -n "Valid unitl (YYYY/MM/DD HH:MM:SS) if none now + $VALID_DAYS days: "
		read EDATE
	fi
}
get_cn() {
	$X509 -in $1  -subject -noout 2>/dev/null | awk 'BEGIN {RS="/" } $0 ~ /^CN=/ { print substr($0,4)}'
}

move_expired_cert() {
# $1 cert-code

	mkdir -p $CAEXP
    	mv $CARCH/$1-cert.pem $CAEXP
	if [ -f $CARCH/$1.p12 ]; then
    		mv $CARCH/$1.p12 $CAEXP
	fi
	find $CARCH -lname  $1* -exec /bin/mv \{\} $CAEXP \;
}

move_revoked_cert() {
# $1 cert-code

	mkdir -p $CAREV
    	mv $CARCH/$1-req.pem $CAREV
    	mv $CARCH/$1-cert.pem $CAREV
	if [ -f $CARCH/$1.p12 ]; then
    		mv $CARCH/$1.p12 $CAREV
	fi
	find $CARCH -lname  $1* -exec /bin/mv \{\} $CAREV \;
}

get_period() {
	# Ask start date
	
	get_sdate
	
	if [ "$SDATE" == "" ]; then
        	echo "Date empty using current date"
        	STARTDATE=""
	else
        	STARTDATE="-startdate `date -d "$SDATE" +"%y%m%d%H%M%S%z"`"
	fi
	
	get_edate
	
	if [ "$EDATE" == "" ]; then
		echo "Date empty using 1 year time expiry"
        if [ "$STARTDATE" == "" ]; then
                ENDDATE="-days $VALID_DAYS"
        else
                XADATE=`date +"%Y" -d "$SDATE"`
                let XADATE++
                XEDATE=$XADATE"/"`date +"%m/%d %H:%M:%S"`
                ENDDATE="-enddate `date -d "$XEDATE" +"%y%m%d%H%M%S%z"`"
        fi
    	else
        	ENDDATE="-enddate `date -d "$EDATE" +"%y%m%d%H%M%S%z"`"
    	fi
}

newca() {
	syncronize
	if [ -d $CATOP ]; then
		error "Directory $CATOP Exist."
	fi
    	if [ ! -f ${CATOP}/serial ]; then
        	# create the directory hierarchy
	        mkdir ${CATOP}
	        mkdir ${CATOP}/certs
	        mkdir ${CATOP}/crl
	        mkdir ${CATOP}/newcerts
	        mkdir ${CATOP}/private
	        echo "01" > ${CATOP}/serial
	        touch ${CATOP}/index.txt
		mkdir -p $CAREV
		mkdir -p $CARCH
		mkdir -p $PRIVATE_DIR
		mkdir -p $BACKUP_DIR
		touch $PWD_FILE
	fi
	if [ ! -f ${CATOP}/private/$CAKEY ]; then
        	echo "CA certificate filename (or enter to create)"
	        read FILE

        # ask user for existing CA certificate
        if [ "$FILE" ]; then
           	cp $FILE ${CATOP}/private/$CAKEY
            	RET=$?
        else
            	echo "Making CA certificate ..."
            	$REQ -new -x509 -extensions v3_ca -keyout $CAKEY \
                           -out $CACERT -days $CA_DAYS -config $OPENSSL_CONF
            	RET=$?
        fi
    fi

}

gen_pkcs() {

# Build PKCS12 Certificate

    	echo "Building PKCS12 Certificate"
    	
	if [ -z $PRIVPASS ]; then
		PASSIN=""
	else
		PASSIN="-passin pass:$PRIVPASS"
	fi
	
	$PKCS12 -export \
        	-inkey $NEWREQ $PASSIN \
        	-in $NEWCERT -name  "$PKCS_NAME" -certfile "$CACERT" -caname "$PKCS_CA_NAME" \
        	-out $P12_TMP_CERT_NAME $PASS_OUT

    	if [ $? != "0" ]; then
        	echo "Error converting certificate to PKCS#12 form!"
        	echo "Aborting ..."
        	exit 1
    	fi

    	echo "PKCS#12 Certificate built!"

}


newcert() {
	syncronize
	if [ "$2" != "nopass" ]; then
		
		get_certpass	
		
		PASS_OUT="-passout pass:$PRIVPASS"
	else
		PASS_OUT="-nodes"
	fi

	get_ca_pass
	
	get_period

	# create a certificate
	NEWREQ=newreq.pem
	NEWCERT=newcert.pem
	$REQ -new -keyout $NEWREQ -out $NEWREQ $PASS_OUT
	if [ $? != "0" ]; then
        	echo "Error making certificate request!"
        	echo "Aborting ..."
        	exit 1
    	fi

    	echo "Signing Certificate ..."
    	$CA -out $NEWCERT -in $NEWREQ $STARTDATE $ENDDATE -key "$CAPASS"
    	if [ $? != "0" ]; then
        	echo "Error signing certificate!"
        	echo "Aborting ..."
        	exit 1
    	fi

    	echo "Certificate Signed"
	
	if [ "$2" != "nopass" ]; then
		gen_pkcs
	fi
	# Archive Certificates

    	echo "Archiving Certificate"
    	ARCDATE=`date +"%y%m%d%H%M%S"`
    	mkdir -p $CARCH
    	mv $NEWREQ $CARCH/$ARCDATE-req.pem
    	mv $NEWCERT $CARCH/$ARCDATE-cert.pem
	
	CN=`get_cn $CARCH/$ARCDATE-cert.pem`
	
	if [ -f $P12_TMP_CERT_NAME ]; then
	    	mv $P12_TMP_CERT_NAME $CARCH/$ARCDATE.p12
		ln -s $ARCDATE.p12 "$CARCH/$CN.p12"
	fi
	echo "Certificate code: $ARCDATE"
	ln -s $ARCDATE-cert.pem "$CARCH/$CN-cert.pem"
	ln -s $ARCDATE-req.pem "$CARCH/$CN-req.pem"
	
	if [ $PRIVPASS ];then
		if [ -n "$PWD_FILE" ];then
			decrypt $PWD_FILE.gpg $PWD_FILE
			echo "Saving Password in encrypted file $PWD_FILE.gpg"
			echo "$CN:$ARCDATE:$PRIVPASS" >> $PWD_FILE
			encrypt $PWD_FILE
		fi
	fi
    	echo "Certificates archived!"
    	exit 0

}

gencrl() {
	syncronize
	# New CRL
    	echo "Generating new CRL"
	
	get_ca_pass	

    	$CA -gencrl -crldays 365 -out $CRL_FILE_NAME.pem -key "$CAPASS"
    	if [ $? != "0" ]; then
        	echo "Error generating Certificate Revocation List!!"
        	echo "Aborting..."
        	exit 1
    	fi

	# Convert CRL to DER format
    	$CRL -in $CRL_FILE_NAME.pem -outform DER -out $CRL_FILE_NAME.der
    	if [ $? != "0" ]; then
        	echo "Error converting Certificate Revocation List!!"
        	echo "Aborting..."
        	exit 1
    	fi
    	#rm -f $CRL_FILE_NAME.pem
}


revoke() {
	syncronize
	get_ca_pass	
	
	echo -n "Revocation code: "
    	read REVCODE
    	echo ""
    	if [ ! -f $CARCH/$REVCODE-cert.pem ]; then
        	echo "Certificate not found!"
        	echo "Aborting..."
       	 	exit 1
    	fi
    	$CA -revoke $CARCH/$REVCODE-cert.pem -key "$CAPASS"
    	if [ $? != "0" ]; then
        	echo "Error revoking Certificate!"
        	echo "Aborting..."
        	exit 1
    	fi
    	echo "Certificate revoked!"
	# Move certicate to revoked repository
    	move_revoked_cert $REVCODE

}

renew() {
	syncronize

	if [ "$2" != "nopass" ]; then
    		# Fetch certificate password
		echo -n "Certificate password: "
	    	read -s PRIVPASS
		echo ""
		echo -n "Certificate password (confirm): "
		read -s PRIVPASS2
		echo ""
		if [ "$PRIVPASS" != "$PRIVPASS2" ]; then
			echo "Password mismatch!"
			exit 1
		fi
		
		PASS_OUT="-passout pass:$PRIVPASS"
	else
		PASS_OUT="-nodes"
		#PKCS=0
	fi

	get_ca_pass
	
	echo -n "Renew code: "
    	read RENEWCODE
    	echo ""

    	if [ ! -f $CARCH/$RENEWCODE-cert.pem ]; then
        	echo "Certificate not found!"
        	echo "Aborting..."
       	 	exit 1
    	fi
	
	move_expired_cert $RENEWCODE
	
	get_period

	$CA -policy policy_anything -out  $CARCH/$RENEWCODE-cert.pem -in $CARCH/$RENEWCODE-req.pem $STARTDATE $ENDDATE -key "$CAPASS"
	
	NEWREQ=$CARCH/$RENEWCODE-req.pem
	NEWCERT=$CARCH/$RENEWCODE-cert.pem

	if [ "$2" != "nopass" ]; then	
		gen_pkcs
	fi
	
	CN=`get_cn $CARCH/$ARCDATE-cert.pem`

	if [ -f $P12_TMP_CERT_NAME ]; then
	    	mv $P12_TMP_CERT_NAME $CARCH/$RENEWCODE.p12
		ln -s $RENEWCODE.p12 "$CARCH/$CN.p12"
	fi
	echo "Certificate code: $ARCDATE"
	ln -s $RENEWCODE-cert.pem "$CARCH/$CN-cert.pem"
	
	if [ $PRIVPASS ];then
		if [ -n "$PWD_FILE" ];then
			decrypt $PWD_FILE.gpg $PWD_FILE
			echo "Saving Password in encrypted file $PWD_FILE.gpg"
			echo "$CN:$RENEWCODE:$PRIVPASS" >> $PWD_FILE
			encrypt $PWD_FILE
		fi
	fi
    	echo "Certificates archived!"

}

pubcrl(){
	# Publish CRL File
#	net_up
	if [ "$2" == "all" ];then
		for a in $CRL_PLUGINS_DIR/*
		do
			if [ -x $a ];then
				$a $CRL_FILE_NAME
			fi
		done

	elif [ -x $CRL_PLUGINS_DIR/$2 ]; then
		$CRL_PLUGINS_DIR/$2 $CRL_FILE_NAME
	else
		echo "Error: plugin $2 not found"
	fi
#	net_down
	exit 0
}

#############################################################################
# Purpose: Convert a sstring month into a integer representation
# Arguments:
#   $1 -> Month name (e.g., Sep)
#############################################################################

getmonth(){
       case ${1} in
             Jan) echo 1 ;;
             Feb) echo 2 ;;
             Mar) echo 3 ;;
             Apr) echo 4 ;;
             May) echo 5 ;;
             Jun) echo 6 ;;
             Jul) echo 7 ;;
             Aug) echo 8 ;;
             Sep) echo 9 ;;
             Oct) echo 10 ;;
             Nov) echo 11 ;;
             Dec) echo 12 ;;
               *) echo  0 ;;
       esac
}

#############################################################################
# Purpose: Calculate the number of seconds between two dates
# Arguments:
#   $1 -> Date #1
#   $2 -> Date #2
#############################################################################

date_diff(){
        if [ "${1}" != "" ] &&  [ "${2}" != "" ]
        then
                echo $(expr ${2} - ${1})
        else
                echo 0
        fi
}

#############################################################################
# Purpose: Convert a date from MONTH-DAY-YEAR to Julian format
# Acknowledgements: Code was adapted from examples in the book
#                   "Shell Scripting Recipes: A Problem-Solution Approach"
#                   ( ISBN 1590594711 )
# Arguments:
#   $1 -> Month (e.g., 06)
#   $2 -> Day   (e.g., 08)
#   $3 -> Year  (e.g., 2006)
#############################################################################
date2julian() {

        if [ "${1} != "" ] && [ "${2} != ""  ] && [ "${3}" != "" ]
        then
                ## Since leap years add aday at the end of February, 
                ## calculations are done from 1 March 0000 (a fictional year)
                d2j_tmpmonth=$((12 * ${3} + ${1} - 3))

                ## If it is not yet March, the year is changed to the previous year
                d2j_tmpyear=$(( ${d2j_tmpmonth} / 12))

                ## The number of days from 1 March 0000 is calculated
                ## and the number of days from 1 Jan. 4713BC is added 
                echo $(( (734 * ${d2j_tmpmonth} + 15) / 24 -  2 * ${d2j_tmpyear} + ${d2j_tmpyear}/4
                              - ${d2j_tmpyear}/100 + ${d2j_tmpyear}/400 + $2 + 1721119 ))
        else
                echo 0
        fi
}


check_file(){
        FILE=$1
	CERT_NAME=`namei $FILE | grep " - " | cut -d \  -f 3 | cut -d . -f 1 | cut -d - -f 1`
	MONTH=$(${DATE} "+%m")
	DAY=$(${DATE} "+%d")
	YEAR=$(${DATE} "+%Y")

	NOWJULIAN=$(date2julian ${MONTH#0} ${DAY#0} ${YEAR})

        ### Grab the expiration date from the X.509 certificate
        CERTDATE=$($X509 -in $FILE -enddate -noout 2>/dev/null | sed 's/notAfter\=//')
        #CERTISSUER=$($X509 -in $FILE -subject -noout 2>/dev/null | awk 'BEGIN {RS="/" } $0 ~ /^CN=/ { print substr($0,4,17)}')
	CERTISSUER=`get_cn $FILE`
        
	if [ "$CERTDATE" = "" ]
        then
               	return 0
        fi

        ### Split the result into parameters, and pass the relevant pieces to date2julian
        set -- $CERTDATE
        MONTH=$(getmonth $1)
       	
	# Convert the date to seconds, and get the diff between NOW and the expiration date
        CERTJULIAN=$(date2julian ${MONTH#0} ${2#0} ${4})
        CERTDIFF=$(date_diff ${NOWJULIAN} ${CERTJULIAN})

        if [ ${CERTDIFF} -lt 0 ]
        then
                if [ "${ALARM}" = "TRUE" ]
                then
                        echo "The SSL certificate ${FILE} has expired!" \
                        | ${MAIL} -s "Certificate ${FILE} has expired!" $CAS_ADMIN
                fi

                echo -e $CERT_NAME \\t EXPIRED \\t ${CERTDATE} \\t\\t ${CERTDIFF} \\t $CERTISSUER \\t

        elif [ ${CERTDIFF} -lt ${WARNDAYS} ]
        then
                if [ "${ALARM}" = "TRUE" ]
                then
                        echo "The SSL certificate ${FILE} will expire on ${CERTDATE}" \
                        | ${MAIL} -s "Certificate ${FILE} will expire on ${CERTDATE}" $CAS_ADMIN
                fi
                
                echo -e $CERT_NAME \\t Expiring \\t ${CERTDATE} \\t\\t ${CERTDIFF} \\t $CERTISSUER \\t
        else
                echo -e $CERT_NAME \\t Valid \\t ${CERTDATE} \\t\\t ${CERTDIFF} \\t $CERTISSUER \\t
        fi
}

check(){
	echo -e CERT \\t\\t STATUS \\t EXP-DATE \\t\\t TIME_LEFT \\t ISSUER\ \\t
	echo ---------------------------------------------------------------------------------
	# FIXME
		
	for f in `find $CARCH/ -type f -name *-cert.pem`
	do
		check_file $f
	done
	exit 0
}


for i
do
case $i in

\?|h|help)
	echo "usage: $0 newcert [nopass]|revoke|pubcrl [all|{plugin_name}]|newca|gencrl|check [cert_path]| backup | renew certcode" >&2
	echo '		Environment variable: 	$CAPASS		CA Passphrase'
 	echo '					$PRIVPASS	Private key passphrase'
	echo '					$STATE		Validity Not Before - empty for now'
	echo '					$EDATE		Validity Not After - emty for now + VALID_DAYS (see config file)'
	exit 0
	;;
newca)
	newca
	exit 0
	;;
newcert)
	newcert $*
	exit 0
	;;

revoke)
	revoke
	gencrl
	exit 0
	;;
gencrl)
	gencrl
	exit 0
	;;
pubcrl)
	pubcrl $*
	exit 0
	;;
check)
	check
	exit 0
	;;
backup)
	backup
	exit 0
	;;
renew)
	renew $*
	exit 0
	;;
*)
		$0 h
	exit 0
	;;
esac
done

$0 h
